using System.Text;
using MagicOnion.Client.SourceGenerator.CodeAnalysis;

namespace MagicOnion.Client.SourceGenerator.CodeGen;

public class StaticStreamingHubClientGenerator
{
    class StreamingHubClientBuildContext
    {
        public StreamingHubClientBuildContext(MagicOnionStreamingHubInfo hub, StringBuilder writer, bool enableStreamingHubDiagnosticHandler)
        {
            Hub = hub;
            Writer = writer;
            EnableStreamingHubDiagnosticHandler = enableStreamingHubDiagnosticHandler;
        }

        public MagicOnionStreamingHubInfo Hub { get; }

        public StringBuilder Writer { get; }

        public bool EnableStreamingHubDiagnosticHandler { get; }
    }

    public static string Build(GenerationContext generationContext, MagicOnionStreamingHubInfo hubInfo)
    {
        using var pooledStringBuilder = generationContext.GetPooledStringBuilder();
        var writer = pooledStringBuilder.Instance;

        EmitHeader(generationContext, writer);

        var buildContext = new StreamingHubClientBuildContext(hubInfo, writer, generationContext.Options.EnableStreamingHubDiagnosticHandler);

        EmitPreamble(generationContext, buildContext);
        EmitHubClientClass(generationContext, buildContext);
        EmitPostscript(generationContext, buildContext);

        return writer.ToString();
    }

    static void EmitHeader(GenerationContext generationContext, StringBuilder writer)
    {
        writer.AppendLine("""
            // <auto-generated />
            #pragma warning disable CS0618 // 'member' is obsolete: 'text'
            #pragma warning disable CS0612 // 'member' is obsolete
            #pragma warning disable CS0414 // The private field 'field' is assigned but its value is never used
            #pragma warning disable CS8019 // Unnecessary using directive.
            #pragma warning disable CS1522 // Empty switch block
            #pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously.

            """);
    }

    static void EmitPreamble(GenerationContext generationContext, StreamingHubClientBuildContext ctx)
    {
        if (!string.IsNullOrWhiteSpace(generationContext.Namespace))
        {
            ctx.Writer.AppendLineWithFormat($$"""
            namespace {{generationContext.Namespace}}
            {
            """);
        }
        ctx.Writer.AppendLineWithFormat($$"""
                partial class {{generationContext.InitializerPartialTypeName}}
                {
                    static partial class MagicOnionGeneratedClient
                    {
            """);
    }

    static void EmitPostscript(GenerationContext generationContext, StreamingHubClientBuildContext ctx)
    {
        ctx.Writer.AppendLine("""
                    }
                }
            """);

        if (!string.IsNullOrWhiteSpace(generationContext.Namespace))
        {
            ctx.Writer.AppendLine("""
            }
            """);
        }
    }

    static void EmitProperties(StreamingHubClientBuildContext ctx)
    {
        if (ctx.EnableStreamingHubDiagnosticHandler)
        {
            ctx.Writer.AppendLineWithFormat($"""
                            readonly global::MagicOnion.Client.IStreamingHubDiagnosticHandler diagnosticHandler;
            """);
            ctx.Writer.AppendLine();
        }
    }

    static void EmitHubClientClass(GenerationContext generationContext, StreamingHubClientBuildContext ctx)
    {
        ctx.Writer.AppendLineWithFormat($$"""
                        [global::MagicOnion.Ignore]
                        public class {{ctx.Hub.GetClientFullName()}} : global::MagicOnion.Client.StreamingHubClientBase<{{ctx.Hub.ServiceType.FullName}}, {{ctx.Hub.Receiver.ReceiverType.FullName}}>, {{ctx.Hub.ServiceType.FullName}}
                        {
            """);
        EmitProperties(ctx);
        EmitConstructor(ctx);
        EmitHelperMethods(ctx);
        EmitHubMethods(ctx, isFireAndForget: false);
        EmitFireAndForget(ctx);
        EmitOnBroadcastEvent(ctx);
        EmitOnResponseEvent(ctx);
        EmitOnClientResultEvent(ctx);
        ctx.Writer.AppendLine("""
                        }
            """);
        // }
    }

    static void EmitHelperMethods(StreamingHubClientBuildContext ctx)
    {
        if (ctx.EnableStreamingHubDiagnosticHandler)
        {
            ctx.Writer.AppendLineWithFormat($$"""
                            global::System.Threading.Tasks.Task<TResponse> WriteMessageWithResponseDiagnosticTaskAsync<TRequest, TResponse>(int methodId, TRequest message, [global::System.Runtime.CompilerServices.CallerMemberName] string callerMemberName = default!)
                            {
                                if (diagnosticHandler is null)
                                {
                                    return base.WriteMessageWithResponseTaskAsync<TRequest, TResponse>(methodId, message);
                                }

                                return diagnosticHandler.OnMethodInvoke(this, methodId, callerMemberName, message, isFireAndForget: true, base.WriteMessageWithResponseValueTaskOfTAsync<TRequest, TResponse>).AsTask();
                            }

                            async global::System.Threading.Tasks.ValueTask WriteMessageWithResponseDiagnosticValueTaskAsync<TRequest, TResponse>(int methodId, TRequest message, [global::System.Runtime.CompilerServices.CallerMemberName] string callerMemberName = default!)
                            {
                                if (diagnosticHandler is null)
                                {
                                    await base.WriteMessageWithResponseValueTaskAsync<TRequest, TResponse>(methodId, message);
                                    return;
                                }

                                await diagnosticHandler.OnMethodInvoke(this, methodId, callerMemberName, message, isFireAndForget: true, base.WriteMessageWithResponseValueTaskOfTAsync<TRequest, TResponse>);
                            }

                            global::System.Threading.Tasks.ValueTask<TResponse> WriteMessageWithResponseDiagnosticValueTaskOfTAsync<TRequest, TResponse>(int methodId, TRequest message, [global::System.Runtime.CompilerServices.CallerMemberName] string callerMemberName = default!)
                            {
                                if (diagnosticHandler is null)
                                {
                                    return base.WriteMessageWithResponseValueTaskOfTAsync<TRequest, TResponse>(methodId, message);
                                }

                                return diagnosticHandler.OnMethodInvoke(this, methodId, callerMemberName, message, isFireAndForget: true, base.WriteMessageWithResponseValueTaskOfTAsync<TRequest, TResponse>);
                            }

                            global::System.Threading.Tasks.Task<TResponse> WriteMessageFireAndForgetDiagnosticTaskAsync<TRequest, TResponse>(int methodId, TRequest message, [global::System.Runtime.CompilerServices.CallerMemberName] string callerMemberName = default!)
                            {
                                if (diagnosticHandler is null)
                                {
                                    return base.WriteMessageFireAndForgetTaskAsync<TRequest, TResponse>(methodId, message);
                                }

                                return diagnosticHandler.OnMethodInvoke(this, methodId, callerMemberName, message, isFireAndForget: true, base.WriteMessageFireAndForgetValueTaskOfTAsync<TRequest, TResponse>).AsTask();
                            }

                            async global::System.Threading.Tasks.ValueTask WriteMessageFireAndForgetDiagnosticValueTaskAsync<TRequest, TResponse>(int methodId, TRequest message, [global::System.Runtime.CompilerServices.CallerMemberName] string callerMemberName = default!)
                            {
                                if (diagnosticHandler is null)
                                {
                                    await base.WriteMessageFireAndForgetTaskAsync<TRequest, TResponse>(methodId, message);
                                    return;
                                }

                                await diagnosticHandler.OnMethodInvoke(this, methodId, callerMemberName, message, isFireAndForget: true, base.WriteMessageFireAndForgetValueTaskOfTAsync<TRequest, TResponse>);
                            }

                            global::System.Threading.Tasks.ValueTask<TResponse> WriteMessageFireAndForgetDiagnosticValueTaskOfTAsync<TRequest, TResponse>(int methodId, TRequest message, [global::System.Runtime.CompilerServices.CallerMemberName] string callerMemberName = default!)
                            {
                                if (diagnosticHandler is null)
                                {
                                    return base.WriteMessageFireAndForgetValueTaskOfTAsync<TRequest, TResponse>(methodId, message);
                                }
                            
                                return diagnosticHandler.OnMethodInvoke(this, methodId, callerMemberName, message, isFireAndForget: true, base.WriteMessageFireAndForgetValueTaskOfTAsync<TRequest, TResponse>);
                            }
            """);
            ctx.Writer.AppendLine();
        }
    }

    static void EmitConstructor(StreamingHubClientBuildContext ctx)
    {
        if (ctx.EnableStreamingHubDiagnosticHandler)
        {
            ctx.Writer.AppendLineWithFormat($$"""
                            public {{ctx.Hub.GetClientFullName()}}({{ctx.Hub.Receiver.ReceiverType.FullName}} receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options, global::MagicOnion.Client.IStreamingHubDiagnosticHandler diagnosticHandler)
                                : base("{{ctx.Hub.ServiceType.Name}}", receiver, callInvoker, options)
                            {
                                this.diagnosticHandler = diagnosticHandler;
                            }
            """);
        }
        else
        {
            ctx.Writer.AppendLineWithFormat($$"""
                            public {{ctx.Hub.GetClientFullName()}}({{ctx.Hub.Receiver.ReceiverType.FullName}} receiver, global::Grpc.Core.CallInvoker callInvoker, global::MagicOnion.Client.StreamingHubClientOptions options)
                                : base("{{ctx.Hub.ServiceType.Name}}", receiver, callInvoker, options)
                            {
                            }
            """);
        }
        ctx.Writer.AppendLine();
    }

    static void EmitFireAndForget(StreamingHubClientBuildContext ctx)
    {
        ctx.Writer.AppendLineWithFormat($$"""
                            public {{ctx.Hub.ServiceType.FullName}} FireAndForget()
                                => new FireAndForgetClient(this);

                            [global::MagicOnion.Ignore]
                            class FireAndForgetClient : {{ctx.Hub.ServiceType.FullName}}
                            {
                                readonly {{ctx.Hub.GetClientFullName()}} parent;

                                public FireAndForgetClient({{ctx.Hub.GetClientFullName()}} parent)
                                    => this.parent = parent;

                                public {{ctx.Hub.ServiceType.FullName}} FireAndForget() => this;
                                public global::System.Threading.Tasks.Task DisposeAsync() => throw new global::System.NotSupportedException();
                                public global::System.Threading.Tasks.Task WaitForDisconnect() => throw new global::System.NotSupportedException();

            """);
        EmitHubMethods(ctx, isFireAndForget: true);
        ctx.Writer.AppendLine("""
                            }

            """);
    }

    static void EmitHubMethods(StreamingHubClientBuildContext ctx, bool isFireAndForget)
    {
        // public Task MethodReturnWithoutValue()
        //     => WriteMessageWithResponseAsync<MessagePack.Nil, MessagePack.Nil>(FNV1A32.GetHashCode(nameof(MethodReturnWithoutValue)), MessagePack.Nil.Default);
        // public Task<int> MethodParameterless()
        //     => WriteMessageWithResponseAsync<MessagePack.Nil, int>(FNV1A32.GetHashCode(nameof(MethodParameterless)), MessagePack.Nil.Default);
        // public Task<int> MethodParameter_One(int arg0)
        //     => WriteMessageWithResponseAsync<int, int>(FNV1A32.GetHashCode(nameof(MethodParameter_One)), arg0);
        // public Task<int> MethodParameter_Many(int arg0, string arg1)
        //     => WriteMessageWithResponseAsync<DynamicArgumentTuple<int, string>, int>(FNV1A32.GetHashCode(nameof(MethodParameter_Many)), new DynamicArgumentTuple<int, string>(arg0, arg1));
        foreach (var method in ctx.Hub.Methods)
        {
            var writeMessageParameters = method.Parameters.Count switch
            {
                // Nil.Default
                0 => $", global::MessagePack.Nil.Default",
                // arg0
                1 => $", {method.Parameters[0].Name}",
                // new DynamicArgumentTuple(arg1, arg2, ...)
                _ => $", {method.Parameters.ToNewDynamicArgumentTuple()}",
            };
            var isReturnTypeVoid = method.MethodReturnType == MagicOnionTypeInfo.KnownTypes.System_Void;
            var writeMessageTarget = isFireAndForget ? "parent" : "this";
            var writeMessageAsyncPrefix = ctx.EnableStreamingHubDiagnosticHandler
                ? isFireAndForget || isReturnTypeVoid
                    ? $"{writeMessageTarget}.WriteMessageFireAndForgetDiagnostic"
                    : $"{writeMessageTarget}.WriteMessageWithResponseDiagnostic"
                : isFireAndForget || isReturnTypeVoid
                    ? $"{writeMessageTarget}.WriteMessageFireAndForget"
                    : $"{writeMessageTarget}.WriteMessageWithResponse";

            if (isFireAndForget) ctx.Writer.Append("    ");
            ctx.Writer.AppendLineWithFormat($"""
                            public {(isReturnTypeVoid ? "void" : method.MethodReturnType.FullName)} {method.MethodName}({method.Parameters.ToMethodSignaturize()})
            """);

            if (isFireAndForget) ctx.Writer.Append("    ");
            if (method.MethodReturnType == MagicOnionTypeInfo.KnownTypes.System_Threading_Tasks_ValueTask)
            {
                // ValueTask
                ctx.Writer.AppendLineWithFormat($"""
                                => {writeMessageAsyncPrefix}ValueTaskAsync<{method.RequestType.FullName}, {method.ResponseType.FullName}>({method.HubId}{writeMessageParameters});
            """);
            }
            else if (isReturnTypeVoid)
            {
                // void
                ctx.Writer.AppendLineWithFormat($"""
                                => _ = {writeMessageAsyncPrefix}ValueTaskAsync<{method.RequestType.FullName}, {method.ResponseType.FullName}>({method.HubId}{writeMessageParameters});
            """);
            }
            else if (method.MethodReturnType.HasGenericArguments && method.MethodReturnType.GetGenericTypeDefinition() == MagicOnionTypeInfo.KnownTypes.System_Threading_Tasks_ValueTask)
            {
                // ValueTask<T>
                ctx.Writer.AppendLineWithFormat($"""
                                => {writeMessageAsyncPrefix}ValueTaskOfTAsync<{method.RequestType.FullName}, {method.ResponseType.FullName}>({method.HubId}{writeMessageParameters});
            """);
            }
            else
            {
                // Task, Task<T>
                ctx.Writer.AppendLineWithFormat($"""
                                => {writeMessageAsyncPrefix}TaskAsync<{method.RequestType.FullName}, {method.ResponseType.FullName}>({method.HubId}{writeMessageParameters});
            """);
            }
        }

        ctx.Writer.AppendLine();
    }

    static void EmitOnBroadcastEvent(StreamingHubClientBuildContext ctx)
    {
        ctx.Writer.AppendLine("""
                            protected override void OnBroadcastEvent(global::System.Int32 methodId, global::System.ReadOnlyMemory<global::System.Byte> data)
                            {
            """);
        if (ctx.EnableStreamingHubDiagnosticHandler)
        {
            ctx.Writer.AppendLine("""
                                diagnosticHandler?.OnBroadcastEventRaw(this, methodId, data);
            """);
        }
        ctx.Writer.AppendLine("""
                                switch (methodId)
                                {
            """);
        foreach (var method in ctx.Hub.Receiver.Methods.Where(x => x.MethodReturnType == MagicOnionTypeInfo.KnownTypes.System_Void))
        {
            var methodArgs = method.Parameters.Count switch
            {
                0 => "",
                1 => "value",
                _ => string.Join(", ", Enumerable.Range(1, method.Parameters.Count).Select(x => $"value.Item{x}"))
            };

            ctx.Writer.AppendLineWithFormat($$"""
                                    case {{method.HubId}}: // {{method.MethodReturnType.ToDisplayName()}} {{method.MethodName}}({{method.Parameters.ToMethodSignaturize()}})
                                        {
            """);

            if (ctx.EnableStreamingHubDiagnosticHandler)
            {
                ctx.Writer.AppendLineWithFormat($$"""
                                            var value = base.Deserialize<{{method.RequestType.FullName}}>(data);
                                            diagnosticHandler?.OnBroadcastEvent(this, "{{method.MethodName}}", value);
                                            receiver.{{method.MethodName}}({{methodArgs}});
            """);
            }
            else
            {
                if (method.Parameters.Count != 0)
                {
                    ctx.Writer.AppendLineWithFormat($$"""
                                            var value = base.Deserialize<{{method.RequestType.FullName}}>(data);
            """);
                }
                ctx.Writer.AppendLineWithFormat($$"""
                                            receiver.{{method.MethodName}}({{methodArgs}});
            """);
            }
            ctx.Writer.AppendLine("""
                                        }
                                        break;
            """);
        }
        ctx.Writer.AppendLine("""
                                }
                            }

            """);
    }

    static void EmitOnResponseEvent(StreamingHubClientBuildContext ctx)
    {
        ctx.Writer.AppendLine("""
                            protected override void OnResponseEvent(global::System.Int32 methodId, global::System.Object taskSource, global::System.ReadOnlyMemory<global::System.Byte> data)
                            {
                                switch (methodId)
                                {
            """);
        foreach (var method in ctx.Hub.Methods)
        {
            if (ctx.EnableStreamingHubDiagnosticHandler)
            {
                ctx.Writer.AppendLineWithFormat($$"""
                                    case {{method.HubId}}: // {{method.MethodReturnType.ToDisplayName()}} {{method.MethodName}}({{method.Parameters.ToMethodSignaturize()}})
                                        diagnosticHandler?.OnResponseEvent<{{ctx.Hub.GetClientFullName()}}, {{method.ResponseType.FullName}}>(this, "{{method.MethodName}}", data);
                                        base.SetResultForResponse<{{method.ResponseType.FullName}}>(taskSource, data);
                                        break;
            """);
            }
            else
            {
                ctx.Writer.AppendLineWithFormat($$"""
                                    case {{method.HubId}}: // {{method.MethodReturnType.ToDisplayName()}} {{method.MethodName}}({{method.Parameters.ToMethodSignaturize()}})
                                        base.SetResultForResponse<{{method.ResponseType.FullName}}>(taskSource, data);
                                        break;
            """);

            }
        }
        ctx.Writer.AppendLine("""
                                }
                            }

            """);
    }

    static void EmitOnClientResultEvent(StreamingHubClientBuildContext ctx)
    {
        var clientResultMethods = ctx.Hub.Receiver.Methods.Where(x => x.IsClientResult).ToArray();
        if (clientResultMethods.Length == 0)
        {
            ctx.Writer.AppendLine("""
                            protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory<global::System.Byte> data)
                            {
                            }
            """);
            return;
        }

        ctx.Writer.AppendLine("""
                            protected override void OnClientResultEvent(global::System.Int32 methodId, global::System.Guid messageId, global::System.ReadOnlyMemory<global::System.Byte> data)
                            {
                                try
                                {
                                    switch (methodId)
                                    {
            """);
        foreach (var method in clientResultMethods)
        {
            var methodParameters = method.Parameters
                .Select((x, i) => (Index: i, IsCancellationToken: x.Type == MagicOnionTypeInfo.KnownTypes.System_Threading_CancellationToken, Type: x.Type))
                .ToArray();

            var methodArgs = methodParameters.Count(x => !x.IsCancellationToken) switch
            {
                0 => string.Join(", ", methodParameters.Select(x => "default")),
                1 => string.Join(", ", methodParameters.Select(x => x.IsCancellationToken ? "default" : "value")),
                _ => string.Join(", ", methodParameters.Select(x => x.IsCancellationToken ? "default" : $"value.Item{x.Index + 1}")),
            };

            ctx.Writer.AppendLineWithFormat($$"""
                                        case {{method.HubId}}: // {{method.MethodReturnType.ToDisplayName()}} {{method.MethodName}}({{method.Parameters.ToMethodSignaturize()}})
                                            {
                                                var value = base.Deserialize<{{method.RequestType.FullName}}>(data);
                                                base.AwaitAndWriteClientResultResponseMessage(methodId, messageId, receiver.{{method.MethodName}}({{methodArgs}}));
                                            }
                                            break;
            """);
        }
        ctx.Writer.AppendLine("""
                                    }
                                }
                                catch (global::System.Exception ex)
                                {
                                    base.WriteClientResultResponseMessageForError(methodId, messageId, ex);
                                }
                            }

            """);
    }
}
